Android 蓝牙开发 ᚼᛒ (二)

上一篇对蓝牙协议栈做了一个简要的说明,本篇回到主题,谈谈 Android 中的蓝牙开发。

发展

Android 1.0 开始就支持了蓝牙2.0,开始的时候仅支持 headset profile 和 hands-free profile,1.5(capcake)开始支持了 A2dp,2.0 支持了蓝牙 2.1,并提供了 RFCOMM 相关 API…,到 Android 4.3 终于开始支持低功耗蓝牙,但只支持BLE的中心设备模式,到Android 5.0 才支持外设模式。

虽然一直在加大对蓝牙系统的支持,但在初期的 Android 蓝牙开发中 经常受到诟病的一点是 API 不够稳定,并且还有许多标准通信类型还不支持。其原因主要和使用的蓝牙堆栈实现有关。

在 Android 的早期版本中,整个蓝牙堆栈都是基于 BlueZ 构建的,BlueZ 是 Linux 内核中使用的开源堆栈。这个堆栈非常成熟和稳定。然而在Android环境中,BlueZ 存在一些问题,主要与其附加的 GPL 许可协议有关。为了避免与其他 Apache 许可的 Android 堆栈冲突,BlueZ 需要在一个与特殊守护进程(bluetoothd)交互的单独进程中运行。这些处理带来了大量额外的开销。

为了回避这一切,在 Android 4.2中,Google 用 Bluedroid 取代了 Android 中的 BlueZ,Bluedroid 是博通开发的全新堆栈,采用的是 Apache 开源许可,可以与蓝牙堆栈的其余部分直接兼容。 Bluedroid 是一个全新的堆栈,而 对 BLE 的支持是 Android 4.3 才引入。

Android 蓝牙架构

下面是 Android 8.0 的蓝牙架构图
Android 8.0 蓝牙架构图

Android 中将蓝牙堆栈及上层的JNI、应用程序等作为 Host,供应商提供更底层的蓝牙控制器硬件实现,通过硬件抽象层接口(HIDL)连接到 Android 蓝牙堆栈中。当发生特定蓝牙操作时(如发现设备时),JNI 代码会调用蓝牙堆栈,然后通过 JNI 提供 Java 访问接口。蓝牙系统服务运行在 com.android.bluetooth 进程中,最后通过 Binder 机制向最上层的 APP 层提供 蓝牙相关 API。

Android 为经典蓝牙和低功耗蓝牙提供了不同的API,位于 android.bluetooth 和 android.bluetooth.le 包下。

经典蓝牙

设备查找

设备查找是一个扫描过程,它会搜索附件已启用蓝牙的设备,并请求到关于可检测设备的信息,如设备名称,其唯一 MAC 地址等。利用此信息可以进行接下来的配对/绑定操作。

执行设备查找是一个非常重的操作,在找到目标设备后必须关闭查找。此外,在尝试连接,或者已经连接上某台设备的同时,如果还在执行设备查找操作将大幅减少可用于连接的带宽,可能导致连接失败。

配对/绑定

找到目标设备并获取到基本信息后,可以发起连接。如果是首次连接,将会自动向用户显示配对请求。 设备完成配对后,将会保存关于该设备的基本信息(例如设备名称、类和 MAC 地址),并且可使用 getBondedDevices() 读取这些信息,之后就可以通过保存的已知信息 (MAC 地址) 可随时向其发起连接,而无需执行查找操作。

连接

连接其实不是必须的步骤,与我们使用的蓝牙服务有关。如之前所述,随着 Android 版本的更迭,支持了越来越多的蓝牙特性以及对应各种使用场景的 Profiles, 支持的部分 Profile 如下:
Android 支持的 profiles

对我们开发者而言,这些复杂的协议实现都被封装成了 Java API,我们可以通过这些 API 传输数据并提供各种对应的交互式服务。而每个服务都有一个 UUID 来唯一标识,比如我们通过 RFCOMM 相关的 API 可以提供串口通信服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

static UUID MY_UUID = UUID.fromString("275348FB-C14D-4FD5-B434-7C3F351DEA5F");

// 服务端
BluetoothServerSocket server = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
BluetoothSocket sc = server.accept();

// 客户端
BluetoothSocket client = bluetoothDevice.createRfcommSocketToServiceRecord(MY_UUID);
client.connect();

// 数据传输
OutputStream outStream = socket.getOutputStream();
outStream.write(bytes);

以上就是利用 socket API 作为服务端客户端进行了简单的连接和数据传输。其中listenUsingRfcommWithServiceRecord还有个不安全版:listenUsingInsecureRfcommWithServiceRecord ,这个方法从 API 10 才开始提供,不安全的意思是不进行验证,也不进行连接加密(蓝牙2.1之前),可以用于测试或者一些外设上。客户端和服务端需要使用同一个 UUID 才能通过这个服务进行通信。但是如果我们使用其他的服务,比如A2dp 的时候却没有见到 UUID,难道不需要吗?

其实这些常用的协议 / Profiles 的 UUID 都写在了蓝牙堆栈层(system/bt

1
2
3
4
5
#define UUID_PROTOCOL_SDP 0x0001
#define UUID_PROTOCOL_UDP 0x0002
#define UUID_PROTOCOL_RFCOMM 0x0003
...
#define UUID_SERVCLASS_HEADSET 0X1108

这些内置的协议/服务的uuid 都是16位的,也称为 Assigned Number,是由蓝牙标准组织 SIG 定义的短 UUID,其以一个 BASE_UUID 为基础,对应了一个 128 bit 的标准 UUID,比如SPP 的 Assigned Number为 0x1101 , 那么其完整 UUID 是 00001101-0000-1000-8000-00805F9B34FB.

低功耗蓝牙

有了第一篇对蓝牙规范的了解,Android 里 LE 相关的API就比较容易理解了,主要的GATT / ATT 及其内部属性在Android API 中都有同名的类与之对应。此外,LE 中的 Service、Characteristic 还有 Descriptor 也都是使用 UUID 唯一标示的,和前面的经典蓝牙里 UUID 的定义类似。

在 Bluetooth Low Energy(也称为 Bluetooth Smart)规范中,设备有四个主要角色:

  • 广播 Broadcaster
  • 观察者 Observer
  • 中心设备 Central
  • 外设 Perheral

Google 在Android 5.0 中引入了对 Perheral 和 Broadcaster模式的支持。

Broadcaster / Observe

作为Broadcaster / Observer 应用程序 ,前者我们可以按照一定的间隔,往空中广播数据,后者则在 LE 扫描过程中从设备中接收我们需要的所有内容,而都无需进行连接。每个广播数据包含多个广播数据结构体(AD Structure)的集合,每个结构体包含长度,类型等信息。结构体的数据类型/标识由 SIG 在 GAP 中定义,包括诸如控制标志,设备名称,服务UUID,服务数据和发送功率级别等。广播的数据其实包含两部分:Advertising Data(广播数据) 和 Scan Response Data(扫描响应数据)。通常情况下,广播的一方,按照一定的间隔,往空中广播 Advertising Data,当某个监听设备监听到这个广播数据时候,会通过发送 Scan Response Request,请求广播方发送扫描响应数据数据。这两部分数据的长度都是固定的 31 字节。在 Android 中,系统会把这两个数据拼接在一起,返回一个 62 字节的数组。

AD

Android 5.0 改进了蓝牙扫描的 API,提供了 BluetoothLeScanner,可以通过ScanSettings + ScanFilter 构建器类用于构建合适的扫描请求,使用ScanResult + ScanRecord 分析扫描结果,而不再需要手动解析字节数组。 ScanFilter 引入了更强大的过滤 API ,可以过滤任何扫描参数,而不仅仅是广播的服务 UUID。

*注意:使用 BluetoothLeScanner 只能搜索到LE设备,并且利用BLE的广播 Beacon,可以实现室内定位,与敏感的位置信息相关,因此使用前必须向用户请求地理位置权限。而使用经典蓝牙扫描的方式 : BluetoothAdapterstartDiscovery(),可以搜索到所有类型的蓝牙设备,但是这个操作比较重,一个扫描就要12s,而且这个时间还不能设置,因此如果只涉及 LE 开发,还是应该使用 BluetoothLeScanner

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
BluetoothManager manager = (BluetoothManager)getSystemService(BLUETOOTH_SERVICE);
mBluetoothAdapter = manager.getAdapter();

// 广播 Broadcaster
mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();

AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setConnectable(false)
.setTimeout(0)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
.build();
AdvertiseData data = new AdvertiseData.Builder()
.setIncludeDeviceName(true)
.setIncludeTxPowerLevel(true)
.addServiceUuid(HDP_SERVICE)
.addServiceData(HDP_SERVICE, new byte[] {(byte)value, 0x00})
.build();
// 开始广播
mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback);
...
// 结束广播
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);


// Observe
// 使用 BluetoothLeScanner 进行扫描
mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
// 过滤器,只查找uuid为HDP_SERVICE的设备
ScanFilter beaconFilter = new ScanFilter.Builder()
.setServiceUuid(HDP_SERVICE)
.build();
ArrayList<ScanFilter> filters = new ArrayList<ScanFilter>();
filters.add(beaconFilter);
// 扫描参数设置,设置低延迟
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
// 扫描结果回调
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
Log.d(TAG, "onScanResult");
processResult(result);
}

@Override
public void onBatchScanResults(List<ScanResult> results) {
Log.d(TAG, "onBatchScanResults: "+results.size()+" results");
for (ScanResult result : results) {
processResult(result);
}
}

@Override
public void onScanFailed(int errorCode) {
Log.w(TAG, "LE Scan Failed: "+errorCode);
}

private void processResult(ScanResult result) {
Log.i(TAG, "New LE Device: " + result.getDevice().getName())
}
};

// 开始扫描
mBluetoothLeScanner.startScan(filters, settings, mScanCallback);

...

// 结束扫描
mBluetoothLeScanner.stopScan(mScanCallback);

这里在不需要扫描以后,一定要stopScan,而且 startScanstopScan 中传入的 ScanCallback 一定要是同一个, 这里就不要使用匿名类了。

在Android 4.x上扫描是一项强制设备保持唤醒的操作(使用完全唤醒锁定),在 Android 5.0 中,扫描的新低功耗设置允许应用程序在后台连续扫描而不会过度耗尽电池电量。 通过使用新的过滤 API,我们可以通过限制推送到应用层的广播来进一步减少设备唤醒。当在ScanSettings上设置延迟值时,扫描结果也可以分组进行批量传送。如果在后台扫描,也可以减少设备唤醒。 以上提到的一些功能(部分过滤和扫描结果批处理)是在 HAL 层实现的,因此如果硬件提供商未提供驱动程序支持,某些设备可能无法充分利用,我们可以先通过BluetoothAdapter.isOffloadedFilteringSupported()BluetoothAdapter.isOffloadedScanBatchingSupported()等 API来确定设备是否支持。

关于 Central 和 Perheral 的角色的示例可以参考官网以及这篇博客,介绍的都很详细,不在赘述。

Reference